Skip to content

Avoid converting chunked data to Numpy arrays (i.e. .values calls) - #1588

Open
cmdupuis3 wants to merge 20 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/devalue
Open

Avoid converting chunked data to Numpy arrays (i.e. .values calls)#1588
cmdupuis3 wants to merge 20 commits into
UXARRAY:mainfrom
cmdupuis3:cmd/devalue

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Partly addresses #1583

Overview

This PR is to resolve suboptimal usage of .values throughout the repo, primarily by deferring to lazy xarray and dask operations. See the table of usage sites in Issue #1583.

This specific PR is for the first two tables of usage sites; dead code and bugfixes aren't really included here. Aside from simple cases, some cases were solved by branching on whether the data type was already chunked, so scalability for some parts will depend on if you're using numpy or xarray/dask arrays at those points.

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have updated with any function changes

@cmdupuis3 cmdupuis3 self-assigned this Jul 18, 2026
@cmdupuis3 cmdupuis3 added scalability Related to scalability & performance efforts run-benchmark Run ASV benchmark workflow labels Jul 18, 2026
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [ab023b9] After [ccf801b] Ratio Benchmark (Parameter)
- 521M 336M 0.65 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 632M 336M 0.53 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 660±10ms 510±3ms 0.77 mpas_ocean.CrossSections.time_const_lat('120km', 1)
- 333±1ms 261±1ms 0.79 mpas_ocean.CrossSections.time_const_lat('120km', 2)
- 173±4ms 136±0.9ms 0.78 mpas_ocean.CrossSections.time_const_lat('120km', 4)
- 435M 330M 0.76 mpas_ocean.FaceAreas.peakmem_compute_face_areas('480km')
- 432M 330M 0.76 mpas_ocean.Gradient.peakmem_gradient('480km')

Benchmarks that have stayed the same:

Change Before [ab023b9] After [ccf801b] Ratio Benchmark (Parameter)
232±0.7ms 233±0.4ms 1 bench_connectivity.Connectivity.time_edge_face('120km')
13.4±0.09ms 13.6±0.05ms 1.01 bench_connectivity.Connectivity.time_edge_face('480km')
231±0.7ms 230±0.3ms 1 bench_connectivity.Connectivity.time_edge_node('120km')
12.6±0.07ms 12.7±0.03ms 1.01 bench_connectivity.Connectivity.time_edge_node('480km')
230±0.2ms 230±0.2ms 1 bench_connectivity.Connectivity.time_face_edge('120km')
13.0±0.08ms 12.8±0.07ms 0.99 bench_connectivity.Connectivity.time_face_edge('480km')
823±6ms 841±20ms 1.02 bench_connectivity.Connectivity.time_face_face('120km')
50.8±0.7ms 50.7±0.7ms 1 bench_connectivity.Connectivity.time_face_face('480km')
63.3±0.8μs 64.3±1μs 1.02 bench_connectivity.Connectivity.time_face_node('120km')
62.3±4μs 57.6±2μs 0.93 bench_connectivity.Connectivity.time_face_node('480km')
390±10μs 379±1μs 0.97 bench_connectivity.Connectivity.time_n_nodes_per_face('120km')
315±6μs 307±5μs 0.97 bench_connectivity.Connectivity.time_n_nodes_per_face('480km')
241±9ms 232±0.4ms 0.96 bench_connectivity.Connectivity.time_node_edge('120km')
12.9±0.07ms 12.9±0.03ms 1 bench_connectivity.Connectivity.time_node_edge('480km')
80.6±5ms 79.0±2ms 0.98 bench_connectivity.Connectivity.time_node_face('120km')
4.97±0.1ms 4.84±0.04ms 0.97 bench_connectivity.Connectivity.time_node_face('480km')
334M 334M 1 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
363M 366M 1.01 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
7.45±0.07ms 7.43±0.1ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
2.49±0.01ms 2.49±0.02ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
9.38±0.02ms 9.43±0.01ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.97±0.02ms 1.94±0.01ms 0.98 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.21±0.04μs 1.20±0.06μs 0.99 geometry_kernels.AccucrossKernels.time_accucross
2.56±0.03μs 2.56±0.03μs 1 geometry_kernels.AccucrossKernels.time_accucross_pair
400±4ns 391±5ns 0.98 geometry_kernels.EFTPrimitives.time_acc_sqrt_re
401±20ns 395±5ns 0.99 geometry_kernels.EFTPrimitives.time_diff_of_products
341±5ns 366±10ns 1.07 geometry_kernels.EFTPrimitives.time_two_prod
345±9ns 366±10ns 1.06 geometry_kernels.EFTPrimitives.time_two_sum
1.39±0.04μs 1.38±0.02μs 0.99 geometry_kernels.GCAConstLatIntersection.time_accux_constlat_kernel
1.03±0.01μs 1.01±0.02μs 0.98 geometry_kernels.GCAConstLatIntersection.time_gca_const_lat_intersection
1.79±0.04μs 1.73±0.02μs 0.97 geometry_kernels.GCAConstLatIntersection.time_try_gca_const_lat_intersection
1.52±0.01μs 1.55±0.04μs 1.02 geometry_kernels.GCAGCAIntersection.time_accux_gca_kernel
1.28±0.03μs 1.27±0.07μs 1 geometry_kernels.GCAGCAIntersection.time_gca_gca_intersection
2.02±0.03μs 1.98±0.01μs 0.98 geometry_kernels.GCAGCAIntersection.time_try_gca_gca_intersection
37.7±0.3μs 38.4±0.7μs 1.02 geometry_kernels.OrientPredicates.time_on_minor_arc
717±60ns 681±10ns 0.95 geometry_kernels.OrientPredicates.time_orient3d_on_sphere
2.92±0.1ms 2.81±0.01ms 0.96 geometry_samebody.SameBodyConstLat.time_accux_dispatch
1.34±0.01ms 1.32±0ms 0.99 geometry_samebody.SameBodyConstLat.time_accux_kernel
1.81±0.01ms 1.80±0ms 0.99 geometry_samebody.SameBodyConstLat.time_fp64_dispatch
153±0.8μs 153±0.3μs 1 geometry_samebody.SameBodyConstLat.time_fp64_kernel
32.8±0.1ms 32.8±0.1ms 1 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_dispatch
10.6±0ms 10.6±0ms 1 geometry_samebody_gcagca.SameBodyGcaGca.time_accux_kernel
27.4±0.01ms 27.6±0.07ms 1.01 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_dispatch
4.51±0.03ms 4.69±0.08ms 1.04 geometry_samebody_gcagca.SameBodyGcaGca.time_fp64_kernel
782±4ms 782±5ms 1 import.Imports.timeraw_import_uxarray
2.28±0.03ms 2.28±0.01ms 1 mpas_ocean.CheckNorm.time_check_norm('120km')
1.82±0.02ms 1.84±0.02ms 1.01 mpas_ocean.CheckNorm.time_check_norm('480km')
722±5ms 715±3ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
45.2±0.4ms 45.1±0.3ms 1 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
621±9μs 629±8μs 1.01 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
555±6μs 557±20μs 1 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.05±0.03ms 5.10±0.01ms 1.01 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.53±0.02ms 3.53±0.03ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.33±0.01s 3.31±0.02s 0.99 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
216±4ms 213±0.8ms 0.98 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
19.7±0.03ms 19.7±0.02ms 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.12±0.02ms 1.11±0.01ms 0.99 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.02ms 10.6±0.01ms 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
738±20μs 750±10μs 1.02 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
493±4ms 454±0.9ms 0.92 mpas_ocean.CrossSections.time_const_lat('480km', 1)
247±1ms 231±1ms 0.93 mpas_ocean.CrossSections.time_const_lat('480km', 2)
126±0.4ms 119±0.4ms 0.94 mpas_ocean.CrossSections.time_const_lat('480km', 4)
22.3±0.4ms 22.1±0.1ms 0.99 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.61±0.1ms 2.61±0.02ms 1 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
349M 349M 1 mpas_ocean.FaceAreas.peakmem_compute_face_areas('120km')
57.6±0.3ms 57.5±0.09ms 1 mpas_ocean.FaceAreas.time_compute_face_areas('120km')
5.94±0.06ms 5.96±0.03ms 1 mpas_ocean.FaceAreas.time_compute_face_areas('480km')
838±7ms 837±5ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
50.5±0.5ms 51.3±1ms 1.02 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
72.6±0.3ms 72.6±0.2ms 1 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.67±0.3ms 5.58±0.04ms 0.98 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
350M 350M 1 mpas_ocean.Gradient.peakmem_gradient('120km')
167±3ms 165±0.6ms 0.99 mpas_ocean.Gradient.time_gradient('120km')
12.0±0.4ms 12.0±0.4ms 1 mpas_ocean.Gradient.time_gradient('480km')
368±7μs 369±10μs 1 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
203±4μs 204±10μs 1 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
350M 350M 1 mpas_ocean.Integrate.peakmem_integrate('120km')
329M 328M 1 mpas_ocean.Integrate.peakmem_integrate('480km')
548±10μs 582±10μs 1.06 mpas_ocean.Integrate.time_integrate('120km')
495±20μs 488±5μs 0.99 mpas_ocean.Integrate.time_integrate('480km')
189±2ms 188±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
187±2ms 188±0.7ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
188±2ms 188±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
13.9±0.2ms 14.2±0.3ms 1.02 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
13.8±0.09ms 14.5±0.1ms 1.05 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
14.0±0.2ms 14.2±0.3ms 1.01 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
360±6μs 352±9μs 0.98 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
341±7μs 345±9μs 1.01 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
325±4μs 325±9μs 1 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
317±3μs 314±2μs 0.99 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
213±1ms 212±2ms 1 mpas_ocean.RemapDownsample.time_bilinear_remapping
230±1ms 229±0.5ms 1 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
15.6±0.03ms 15.5±0.07ms 0.99 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.18±0.01s 1.18±0s 1 mpas_ocean.RemapUpsample.time_bilinear_remapping
35.7±0.4ms 36.6±0.6ms 1.03 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
11.4±0.2ms 11.3±0.4ms 1 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
26.6±0.6ms 27.4±0.1ms 1.03 mpas_ocean.ZonalAverage.time_zonal_average('120km')
5.24±0.09ms 5.44±0.3ms 1.04 mpas_ocean.ZonalAverage.time_zonal_average('480km')
324M 324M 1 quad_hexagon.QuadHexagon.peakmem_open_dataset
324M 324M 1 quad_hexagon.QuadHexagon.peakmem_open_grid
6.68±0.2ms 6.48±0.1ms 0.97 quad_hexagon.QuadHexagon.time_open_dataset
5.49±0.1ms 5.53±0.1ms 1.01 quad_hexagon.QuadHexagon.time_open_grid

@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

There's some spurious asv results from machine variability, but the benchmarks consistenly show peak-mem reductions and some speedups for cross-sections.

@cmdupuis3
cmdupuis3 requested a review from erogluorhan July 20, 2026 23:25
@cmdupuis3
cmdupuis3 marked this pull request as ready for review July 20, 2026 23:26
@cmdupuis3
cmdupuis3 requested a review from rajeeja July 23, 2026 23:26
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@Sevans711
Sevans711 self-requested a review July 27, 2026 16:18

@Sevans711 Sevans711 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @cmdupuis3, thank you for proposing these changes! Overall these look like good clean changes which should help improve the scalability of uxarray.

I have some notes/suggestions/requested changes. Primarily, I noticed that there are missing regression tests. This may be especially important for places with significant changes (more significant than just replacing obj.values with obj.data):

  • UxDataArray.integrate()
  • UxDataArrayCrossSectionAccessor.__call__()
  • uxarray.plot.matplotlib._nearest_neighbor_resample()
  • RemapAccessor.apply_weights()

It may be nice to add a regression test for UxDataset.to_xarray() too, even though the changes here are minor, since that feels like a very core part of the functionality.

The tests could be similar to what you added already in test_topological_agg.py (plus my additional request on that file): ensure that numpy and dask inputs ultimately give the same values (plus assert that the dask inputs lead to dask outputs).

Other parts of the code changes here might benefit from similar tests, but I don't know if that should be necessary. For example, the changes in _geos just replace obj.values with obj.data; does a regression test need to be added for that or no? Curious to hear from @erogluorhan and/or @rajeeja on this question in particular.

Misc. note: I'm not sure if I fully understand the changes to uxarray/cross_sections files. I need to make sure to take a closer look at those during a subsequent review.

Comment thread uxarray/core/aggregation.py
Comment thread uxarray/core/aggregation.py Outdated
Comment thread uxarray/core/aggregation.py
Comment thread uxarray/core/aggregation.py Outdated
Comment thread uxarray/core/dataarray.py Outdated
Comment thread uxarray/core/dataarray.py
"i,...i", self.uxgrid.face_areas.values, self.values
)
else:
# dask-backed data: xr.dot keeps the reduction lazy

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does integration need a regression test which is sensitive to dask versus numpy? Perhaps, assert that the result is a dask array, and that the values equal the numpy version, similar to the tests already added by this PR into test_topological_agg.

Comment thread test/core/test_topological_agg.py
@erogluorhan erogluorhan changed the title Devalue uxarray to make it less expensive Avoid converting chunked data to Numpy arrays (i.e. .values calls) in the code Jul 27, 2026
@erogluorhan erogluorhan changed the title Avoid converting chunked data to Numpy arrays (i.e. .values calls) in the code Avoid converting chunked data to Numpy arrays (i.e. .values calls) Jul 27, 2026
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@cmdupuis3
cmdupuis3 requested a review from Sevans711 July 28, 2026 16:54
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

@erogluorhan Sam and I were wondering about your philosophy with respect to dask versus numpy routines. I was thinking that if we have a case where the numpy routine could be faster than a dask routine in some situations, we might want to keep both, or maybe you'd want to go all in on dask for simplicity.

if isinstance(uxda.data, np.ndarray):
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
elif isinstance(uxda.data, da.Array):
    # apply aggregation on dask array, TODO:
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
else:
    raise ValueError

This is something in main's aggregate.py. I fleshed out the dask branch, but now we still have this logic, and I left the numpy routine in on this branch.

@erogluorhan

Copy link
Copy Markdown
Member

@erogluorhan Sam and I were wondering about your philosophy with respect to dask versus numpy routines. I was thinking that if we have a case where the numpy routine could be faster than a dask routine in some situations, we might want to keep both, or maybe you'd want to go all in on dask for simplicity.

if isinstance(uxda.data, np.ndarray):
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
elif isinstance(uxda.data, da.Array):
    # apply aggregation on dask array, TODO:
    aggregated_var = _apply_node_to_face_aggregation_numpy(uxda, ...)
else:
    raise ValueError

This is something in main's aggregate.py. I fleshed out the dask branch, but now we still have this logic, and I left the numpy routine in on this branch.

If I am understanding this correctly, question is about the use of routines, not data structures. If so, using standard NumPy functions directly on chunked Dask-backed Xarray objects should be completely safe with chunks. Let me know if this helps.

@github-project-automation github-project-automation Bot moved this to 👀 In review in UXarray Development Aug 7, 2026
@cmdupuis3 cmdupuis3 moved this from 👀 In review to 🏗 In progress in UXarray Development Aug 7, 2026
@cmdupuis3 cmdupuis3 moved this from 🏗 In progress to 👀 In review in UXarray Development Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-benchmark Run ASV benchmark workflow scalability Related to scalability & performance efforts

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

4 participants